Skip navigation
NintendoAge
Welcome, Guest! Please Login or Join
Loading...

## Nerdy Nights Sound: Part 5 Sound Data, Pointer Tables, Headers

- Posts 1 - 11 of 11
- 1

Sep 16, 2009 at 8:23:31 AM
MetalSlime (0)
(Thomas Hjelm) < Crack Trooper >
Posts: 140 - Joined: 08/14/2008
Japan
Profile
Last Week: Sound Engine Basics, Skeleton sound engine
This Week: Sound Data, Pointer Tables, Headers
Designing Sound Data
We have a skeleton sound engine in place. Time to pack it with flesh and organs. Before we can play a song, we will have to load a song. Before we can load a song, we will need song data. So our next step is to decide how our sound data will look. We'll need to design our data format, create some test data and then build our engine to read and play that data.
Data Formats
So how do we go about designing a sound data format? A good place to start would be to look at what we are aiming to play. We know that our sound engine will have two basic types of sound data:
1. Music
2. Sound Effects (SFX)
Music plays in the background. It uses the first 4 channels, has a tempo, and usually loops over and over again.
Sound Effects are triggered by game events (eg, ball hitting a paddle) and don't loop indefinitely.
Sound effects have the job of communicating to the player what is going on right now, so they have priority over music. If there is music playing on the Square 2 channel, and a sound effect is also using the Square 2 channel, the sound effect should play instead of the music.
Depending on the game, some sound effects may have higher priority than others. For example, in a Zelda-like game the sound of the player taking damage would have priority over the sound of the player swinging their sword. The former communicates critical information to the player while the latter is just for effect.
Streams
As mentioned above, a sound effect will have to share a channel (or channels) with the music. This is unavoidable because music typically uses all the channels at once, all the time. So when a sound effect starts playing, it has to steal a channel (or more) away from the music. The music will continue to play on the other channels, but the shared channel will go to the sound effect. This creates an interesting problem: if we stop music on a channel to play a sound effect, how do we know where to resume the music on that channel when the sound effect is finished?
The answer is that we don't actually stop the music on the shared channel. We still advance it frame by frame in time with the other music channels. We just don't write its data to the APU ports when a sound effect is playing.
To do this, we will need to keep track of multiple streams of sound data. A data stream is a sequence of bytes stored in ROM that the sound engine will read and translate into APU writes. Each stream corresponds to one channel. Music will have 4 data streams - one for each channel. Sound effects will have 2 streams and the sfx themselves will choose which channel(s) they use. So 6 streams total that could potentially be running at the same time. We will number them like this:
MUSIC_SQ1 = $00 ;these are stream number constants
MUSIC_SQ2 = $01 ;stream number is used to index into stream variables (see below)
MUSIC_TRI = $02
MUSIC_NOI = $03
SFX_1 = $04
SFX_2 = $05
Each stream will need it's own variables in RAM. An easy way to organize this is to reserve RAM space in blocks and use the stream number as an index:
;reserve 6 bytes each, one for each stream
stream_curr_sound .rs 6 ;what song/sfx # is this stream currently playing?
stream_channel .rs 6 ;what channel is it playing on?
stream_vol_duty .rs 6 ;volume/duty settings for this stream
stream_note_LO .rs 6 ;low 8 bits of period for the current note playing on the stream
stream_note_HI .rs 6 ;high 3 bits of the note period
;..etc
Here we have 6 bytes reserved for each variable. Each stream gets its own byte, for example:
stream_vol_duty+0: MUSIC_SQ1's volume/duty settings
stream_vol_duty+1: MUSIC_SQ2's volume/duty
stream_vol_duty+2: MUSIC_TRI's on/off
stream_vol_duty+3: MUSIC_NOI's volume
stream_vol_duty+4: SFX_1's volume/duty
stream_vol_duty+5: SFX_2's volume/duty
In our sound_play_frame code we will loop through all of the streams using the stream number as an index:
ldx #$00 ;start at stream 0 (MUSIC_SQ1)
.loop:
;read from data stream in ROM if necessary
;update stream variables based on what we read
lda stream_vol_duty, x ;the value in x determines which stream we are working with
;do stuff with volume
lda stream_note_LO, x
;do stuff with note periods
;do more stuff with other variables
inx ;next stream
cpx #$06 ;loop through all six streams
bne .loop
The music streams will always be running, updating the APU ports with their data frame by frame. When a sound effect starts playing, one or both of the sfx streams will start running. Because our loop processes the SFX streams last, they will write to the APU last and thus overwrite the shared-channel music streams. Our channel conflict is taken care of automatically by the order of our loop!
Music
We now have an idea of how our stream data will be stored in RAM, but there are still many unanswered questions. How do we load a song? How do we know where to find the data streams in ROM? How do we read from those data streams? How do we interpret what we read from those streams?
To answer these questions, we need to make a data format. Let's start with music. What should our music data look like? Most NES music data is divided into three types:
1. Note - what note to play: A3, G#5, C2, etc
2. Note Length - how long to play the notes: eighth note, quarter note, whole note, etc
3. Opcodes - opcodes tell the engine to perform specific tasks: loop, adjust volume, change Duty Cycle for squares, etc
*3.5. Arguments - some opcodes will take arguments as input (e.g. how many times to loop, where to loop to).
Ranges
We will need to design our data format to make it easy for the sound engine to differentiate between these three types of data. We do this by specifying ranges. For example, we might say that byte values of $00-$7F represent Notes. $80-$9F are Note Lengths, and $A0-$FF are opcodes. I just made those numbers up. It really doesn't matter what values we use. The important thing is that we have ranges to test against to determine whether a byte is a note, note length or opcode. In our engine code we will have something like this:
fetch_byte:
lda [sound_pointer], y ;read a byte from the data stream
bpl .note ;if < #$80, it's a Note
cmp #$A0
bcc .note_length ;else if < #$A0, it's a Note Length
.opcode: ;else it's an opcode
;do Opcode stuff
.note_length:
;do Note Length stuff
.note:
;do Note stuff
This code reads a byte from the sound data and then tests that byte to see which range it falls into. It jumps to a different section of code for each possible range. Almost any data format you create will be divided into ranges like this, whether it be sound data, map data, text data, whatever.
BPL and BMI
These two branch instructions are worth learning if you don't know them already. After BEQ, BNE, BCS and BCC they are the most common branch instructions. They are often used in range-testing.
BPL tests the Negative (N) flag and will branch if it is clear. Think of BPL as Branch if PLus. The N flag will be clear if the last instruction executed resulted in a value less than #$80 (ie, bit7 clear).
lda #%01101011
; |
; +-------- bit7 is clear. This will clear the N flag.
bpl .somewhere ;N flag is clear, so this will branch
lda #%10010101
; |
; +-------- bit7 is set. This will set the N flag
bpl .somewhere ;N flag is set, so this will not branch.
BMI is the opposite. It tests the N flag and will branch if it is set. Think of BMI as Branch if MInus. The N flag will be set if the last instruction executed resulted in a value greater than or equal to #$80 (ie, bit7 set).
lda #%01101011
; |
; +-------- bit7 is clear. This will clear the N flag.
bmi .somewhere ;N flag is clear, so this will not branch
lda #%10010101
; |
; +-------- bit7 is set. This will set the N flag
bmi .somewhere ;N flag is set, so this will branch to the label .somewhere.
In the range-testing code above, I used BPL to check if a byte fell into the Note range (00-7F). Go back and check it.
Song Headers
Music on the NES is typically composed of four parts: a Square 1 part, a Square 2 part, a Triangle part and a Noise part. When you want to play a song, you will have the main program issue a command to the sound engine telling it what song you want to play. It will look something like this:
lda #$02
jsr sound_load ;load song 2
Somehow our sound_load subroutine will have to take that "2" and translate it into a whole song, complete with Square 1, Square 2, Triangle and Noise parts. How does that little number become 4 streams of data? Well, that number is an index into a pointer table, a table of pointers to song headers. The song headers themselves will contain pointers to the individual channels' data streams.
Pointer Tables
A pointer table is a special kind of lookup table. Only instead of holding regular old numerical data a pointer table holds addresses. These addresses "point" to the start of data. Addresses on the NES are 16-bit ($0000-$FFFF), so pointer tables are always tables of words. Let's look at an example:
pointer_table:
.word $8000, $ABCD, $CC10, $DF1B
Here we have a pointer table. It's four entries long. Each entry is a 16-bit address. Presumably there is data at these four addresses that we will want to read sometime in our program. To read this data we will need to index into the pointer table, grab the address and store it in a zero-page pointer variable and then read using indirect mode:
.rsset $0000
ptr1 .rs 2 ;a 2-byte pointer variable.
;The first byte will hold the LO byte of an address
;The second byte will hold the HI byte of an address
.org $E000 ;somewhere in ROM
lda #$02 ;the third entry in the pointer table ($CC10)
asl a ;multiply by 2 because we are indexing into a table of words
tay
lda pointer_table, y ;#$10 - little endian, so words are stored LO-byte first
sta ptr1
lda pointer_table+1, y ;#$CC
sta ptr1+1
;now our pointer is setup in ptr1. It "points" to address $CC10. Let's read data from there.
ldy #$00
lda [ptr1], y ;indirect mode. reads the byte at $CC10
sta some_variable
iny
lda [ptr1], y ;reads the byte at $CC11
sta some_other_variable
iny
;... etc
This code takes an index and uses it to read an address from our pointer_table. It stores this address in a variable called ptr1 (LO byte first). Then it reads from this address by using indirect mode. We specify indirect mode by putting []'s around our pointer variable. Look at this instruction:
lda [ptr1], y
It means "Find the address ptr1 is pointing to. Add Y to that address. Load the value at that address into A".
This is very versatile because we can stick any address we want into our ptr1 variable and read from anywhere! A pointer table is just a lookup table of places we want to read from.
Of course you usually won't know where exactly in the ROM your data will be. So instead of declaring addresses explicitely ($8000, $ABCD, $CC10, etc), you will use labels instead:
pointer_table:
.word data1, data2, data3 ;these entries will evaluate to the 16-bit addresses of the labels below
data1:
.byte $FF, $16, $82, $44 ;some random data
data2:
.byte $0E, $EE, $EF, $16, $23
data3:
.byte $00, $01
Song Header Pointer Table
Our songs work the same way. When the main program tells the sound engine to play a song, it will send the song number with it. This song number is actually an index into a pointer table of song headers:
song_headers:
.word song0_header
.word song1_header
.word song2_header
;..etc
sound_load:
asl a ;multiply by 2. we are indexing into a table of pointers (words)
tay
lda song_headers, y ;read LO byte of a pointer from the pointer table.
sta sound_ptr ;sound_ptr is a zero page pointer variable
lda song_headers+1, y ;read HI byte
sta sound_ptr+1
ldy #$00
lda [sound_ptr], y
sta some_variable
iny
;...read the rest of the header data
Header Data
So what will our song header data look like? At the very least it should tell us:
How many data streams we have (songs will usually have 4, but sfx will have fewer)
Which streams those are (which stream index to use)
Which channels those streams use
Where to find those streams (ie, pointers to the beginning of each stream).
Initial values for those streams (for example, initial volume)
As we add more features to our sound engine, we may expand our headers to initialize those features. Let's start simple. Our headers will look like this:
main header:
--------+----------------
byte # | what it tells us
--------+----------------
00 | number of streams
01+ | stream headers (one for each stream)
stream headers:
--------+----------------
byte # | what it tells us
--------+----------------
00 | which stream (stream number)
01 | status byte (see below)
02 | which channel
03 | initial volume (and duty for squares)
04-05 | pointer to data stream
The status byte will be a bit-flag that tells us special information about the stream. For now we will just use bit0 to mark a stream as enabled or disabled. In the future we may use other bits to store other information, such as stream priority.
Stream Status Byte
76543210
|
+- Enabled (0: stream disabled; 1: enabled)
Sample Header
Here is some code showing a sample header:
SQUARE_1 = $00 ;these are channel constants
SQUARE_2 = $01
TRIANGLE = $02
NOISE = $03
MUSIC_SQ1 = $00 ;these are stream # constants
MUSIC_SQ2 = $01 ;stream # is used to index into stream variables
MUSIC_TRI = $02
MUSIC_NOI = $03
SFX_1 = $04
SFX_2 = $05
song0_header:
.byte $04 ;4 streams
.byte MUSIC_SQ1 ;which stream
.byte $01 ;status byte (stream enabled)
.byte SQUARE_1 ;which channel
.byte $BC ;initial volume (C) and duty (10)
.word song0_square1 ;pointer to stream
.byte MUSIC_SQ2 ;which stream
.byte $01 ;status byte (stream enabled)
.byte SQUARE_2 ;which channel
.byte $38 ;initial volume (8) and duty (00)
.word song0_square2 ;pointer to stream
.byte MUSIC_TRI ;which stream
.byte $01 ;status byte (stream enabled)
.byte TRIANGLE ;which channel
.byte $81 ;initial volume (on)
.word song0_tri ;pointer to stream
.byte MUSIC_NOI ;which stream
.byte $00 ;disabled. We will have our load routine skip the
; rest of the reads if the status byte disables the stream.
; We are disabling Noise because we haven't covered it yet.
;these are the actual data streams that are pointed to in our stream headers.
song0_square1:
.byte A3, C4, E4, A4, C5, E5, A5 ;some notes. A minor
song0_square2:
.byte A3, A3, A3, E4, A3, A3, E4 ;some notes to play on square 2
song0_tri:
.byte A3, A3, A3, A3, A3, A3, A3 ;triangle data
Sound Engine Variables
The last thing we need before we can write our sound_load routine is some variables. As mentioned, our sound engine will have several streams running simultaneously. Four will be used for music (one for each tonal channel). Two will be used for sound effects. So we will declare all variables in blocks of 6. Based on our header data, we will need the following variables:
stream_curr_sound .rs 6 ;reserve 6 bytes, one for each stream
stream_status .rs 6
stream_channel .rs 6
stream_vol_duty .rs 6
stream_ptr_LO .rs 6
stream_ptr_HI .rs 6
sound_load
Now let's write some code to read our header. Pay special attention to the X register. I recommend tracing through the code using the sample header above. Here is our sound_load routine:
;-------------------------------------
; load_sound will prepare the sound engine to play a song or sfx.
; input:
; A: song/sfx number to play
sound_load:
sta sound_temp1 ;save song number
asl a ;multiply by 2. We are indexing into a table of pointers (words)
tay
lda song_headers, y ;setup the pointer to our song header
sta sound_ptr
lda song_headers+1, y
sta sound_ptr+1
ldy #$00
lda [sound_ptr], y ;read the first byte: # streams
sta sound_temp2 ;store in a temp variable. We will use this as a loop counter
iny
.loop:
lda [sound_ptr], y ;stream number
tax ;stream number acts as our variable index
iny
lda [sound_ptr], y ;status byte. 1= enable, 0=disable
sta stream_status, x
beq .next_stream ;if status byte is 0, stream disabled, so we are done
iny
lda [sound_ptr], y ;channel number
sta stream_channel, x
iny
lda [sound_ptr], y ;initial duty and volume settings
sta stream_vol_duty, x
iny
lda [sound_ptr], y ;pointer to stream data. Little endian, so low byte first
sta stream_ptr_LO, x
iny
lda [sound_ptr], y
sta stream_ptr_HI, x
.next_stream:
iny
lda sound_temp1 ;song number
sta stream_curr_sound, x
dec sound_temp2 ;our loop counter
bne .loop
rts
Now our sound_load routine is ready. If the main program calls it, like this:
lda #$00 ;song 0
jsr sound_load
Our sound_load routine will take the value in the A register and use it to fill our music RAM with everything we need to get our song running!
Reading Streams
Once we have our header loaded, we are ready to rock. All of our active streams have pointers to their data stored in their stream_ptr_LO and stream_ptr_HI variables. That's all we need to start reading data from them.
To read data from our data stream, we will first copy the stream pointer into a zero-page pointer variable. Then we will read a byte using indirect mode and range-test it to determine whether it is a note, note length or opcode. If it's a note, we will read from our note_table and store the 11-bit period in RAM. Finally, we will update our stream pointer to point to the next byte in the stream.
First we will need to declare some new variable blocks for the note periods:
stream_note_LO .rs 6 ;low 8 bits of period
stream_note_HI .rs 6 ;high 3 bits of period
Here is our se_fetch_byte routine (se_ stands for "sound engine"):
;--------------------------
; se_fetch_byte reads one byte from a sound data stream and handles it
; input:
; X: stream number
se_fetch_byte:
lda stream_ptr_LO, x ;copy stream pointer into a zero page pointer variable
sta sound_ptr
lda stream_ptr_HI, x
sta sound_ptr+1
ldy #$00
lda [sound_ptr], y ;read a byte using indirect mode
bpl .note ;if <#$80, we have a note
cmp #$A0 ;else if <#$A0 we have a note length
bcc .note_length
.opcode: ;else we have an opcode
;nothing here yet
jmp .update_pointer
.note_length:
;nothing here yet
jmp .update_pointer
.note:
asl ;multiply by 2 because we are index into a table of words
sty sound_temp1 ;save our Y register because we are about to destroy it
tay
lda note_table, y ;pull low 8-bits of period and store it in RAM
sta stream_note_LO, x
lda note_table+1, y ;pull high 3-bits of period from our note table
sta stream_note_HI, x
ldy sound_temp1 ;restore the Y register
;update our stream pointers to point to the next byte in the data stream
.update_pointer:
iny ;set index to the next byte in the data stream
tya
clc
adc stream_ptr_LO, x ;add Y to the LO pointer
sta stream_ptr_LO, x
bcc .end
inc stream_ptr_HI, x ;if there was a carry, add 1 to the HI pointer.
.end:
rts
Look at the part that updates the stream pointer. After we finish all our reads, Y will hold the index of the last byte read. To be ready for the next frame, we will want to update our pointer to point to the next byte in the data stream. To do this, we increment Y and add it to the pointer. But we have to be careful here. What if our current position is something like this:
stream_ptr: $C3FF
Y: 1
The next position here should be $C400. But ADC only works on the 8-bit level, so if we add 1 to the low byte of the pointer we will get this instead:
stream_ptr: $C300
The FF in the low byte becomes 00, but the high byte remains the same. We need to increment the high byte manually. But how do we know when to increment it and when to leave it alone? Lucky for us, ADC sets the carry flag whenever it makes a FF->00 transition. So we can just check the carry flag after our addition. If it is set, increment the high byte of the pointer. If it is clear, don't increment it. That's what our code above does.
Playing Music
We've loaded our header. We've set up our stream pointers in RAM. We've written a routine that will read bytes from the streams and turn them into notes. Now we need to update sound_play_frame. sound_play_frame will loop through all 6 streams. It will check the status byte to see if they are enabled. If enabled, it will advance the stream by one frame. Here's the code:
sound_play_frame:
lda sound_disable_flag
bne .done ;if sound engine is disabled, don't advance a frame
inc sound_frame_counter
lda sound_frame_counter
cmp #$08 ;***change this compare value to make the notes play faster or slower***
bne .done ;only take action once every 8 frames.
ldx #$00 ;our stream index. start at MUSIC_SQ1 stream
.loop:
lda stream_status, x ;check bit 0 to see if stream is enabled
and #$01
beq .next_stream ;if disabled, skip to next stream
jsr se_fetch_byte ;read from the stream and update RAM
jsr se_set_apu ;write volume/duty, sweep, and note periods of current stream to the APU ports
.next_stream:
inx
cpx #$06 ;loop through all 6 streams.
bne .loop
lda #$00
sta sound_frame_counter ;reset frame counter so we can start counting to 8 again.
.done:
rts
And here is se_set_apu which will write a stream's data to the APU ports:
se_set_apu:
lda stream_channel, x ;which channel does this stream write to?
asl a
asl a ;multiply by 4 so Y will index into the right set of APU ports (see below)
tay
lda stream_vol_duty, x
sta $4000, y
lda stream_note_LO, x
sta $4002, y
lda stream_note_HI, x
sta $4003, y
lda stream_channel, x
cmp #TRIANGLE
bcs .end ;if Triangle or Noise, skip this part
lda #$08 ;else, set negate flag in sweep unit to allow low notes on Squares
sta $4001, y
.end:
rts
Writing to the APU ports directly like this is actually bad form. We'll learn why in a later lesson.
One thing to pay attention to is how we get our APU port index. We take the channel and multiply it by 4. Recall that we declared constants for our channels:
SQUARE_1 = $00 ;these are channel constants
SQUARE_2 = $01
TRIANGLE = $02
NOISE = $03
If our stream_channel is $00 (SQUARE_1), we multiply by 4 to get $00. y = 0
$4000, y = $4000
$4001, y = $4001
$4002, y = $4002
$4003, y = $4003
If our stream_channel is $01 (SQUARE_2), we multiply by 4 to get $04. y = 4
$4000, y = $4004
$4001, y = $4005
$4002, y = $4006
$4003, y = $4007
If our stream_channel is $02 (TRIANGLE), we multiply by 4 to get $08. y = 8
$4000, y = $4008
$4001, y = $4009 (unused)
$4002, y = $400A
$4003, y = $400B
If our stream_channel is $03 (NOISE), we multiply by 4 to get $0C. y = C
$4000, y = $400C
$4001, y = $400D
$4002, y = $400E
$4003, y = $400F
See how everything lines up nicely?
Putting It All Together
Download and unzip the headers.zip sample files. Make sure the following files are in the same folder as NESASM3:
headers.asm
sound_engine.asm
headers.chr
note_table.i
song0.i
song1.i
song2.i
song3.i
headers.bat
Double click headers.bat. That will run NESASM3 and should produce the headers.nes file. Run that NES file in FCEUXD SP.
Use the controller to select songs and play them. Controls are as follows:
Up: Play
Down: Stop
Right: Next Song
Left: Previous Song
Song0 is a silence song. It is not selectable. headers.asm "plays" song0 to stop the music when you press down. See song0.i to find out how it works.
Song1 is an evil sounding series of minor thirds.
Song2 is a short sound effect on the Sq2 channel. It uses the SFX_1 stream. Try playing it over the other songs to see how it steals the channel from the music.
Song3 is a simple descending chord progression.
Try creating your own songs and sound effects and add them into the mix. To add a new song you will need to take the following steps:
1) create a song header and song data (use the included songs as reference). Note that data streams are terminated with $FF
2) add your header to the song_headers pointer table at the bottom of sound_engine.asm
3) update the constant NUM_SONGS to reflect the new song number total (also at the bottom of sound_engine.asm)
Although not necessary, I recommend keeping your song data in a separate file like I've done with song0.i, song1.i, song2.i and song3.i. This makes it easier to find the data if you need to edit your song later. If you do this, don't forget to .include your file.
Next Week: Timing, Note Lengths, Buffering and Rests
-------------------------
MetalSlime runs away
My nesdev blog: http://tummaigames.com/blog...
Edited: 10/23/2009 at 04:41 AM by MetalSlime
Sep 16, 2009 at 8:24:47 AM
MetalSlime (0)
(Thomas Hjelm) < Crack Trooper >
Posts: 140 - Joined: 08/14/2008
Japan
Profile
This is a monster lesson, and probably the most important of the series. Please take your time with it and ask any questions you have here in the thread.
-------------------------
MetalSlime runs away
My nesdev blog: http://tummaigames.com/blog...
Sep 16, 2009 at 10:21:45 PM
udisi (88)
< King Solomon >
Posts: 3261 - Joined: 11/15/2006
United States
Profile
this is excellent. I actually think week 4 was the big step for me. This one kinda just built off of that one more. I don't understand everything in this one for sure, but I can definitely follow it enough to know what the sections of code are doing. It took me like 10 mins to add my old SFX into this engine and place the updated engine into my game.
I was was thinking about blank notes or breaks and wondering if the easiest way to do it would to be just add a word byte to the note_table.i call BK and have it with a value of $0000. then when writing a song with a rest or something would be like
.byte D3,D4,BK,D4,D3,$FF
I'm learning a lot about pointers, lookup tables, and include from you which is just as useful as the music engine itself. I've always been a bit better at hacking existing code, then designing. I much prefer seeing code in use, then trying to figure out how to design it. After seeing this, it will probably help me a ton on my next game with all kinds of loads. Right now I have seperate subroutines to load my backgrounds and each one of them has the same sets of loops to load the background tiles and attribute table. Now that I kinda see how these lookup tables work, I could cut a ton of code out and use a single loading loop and use a table to decide which backgound and attributes to load.
I tend to be able to always make things work, but I design poorly. These sound tutorials will make me much more efficient in the future.
Sep 17, 2009 at 10:36:19 PM
MetalSlime (0)
(Thomas Hjelm) < Crack Trooper >
Posts: 140 - Joined: 08/14/2008
Japan
Profile
Originally posted by: udisi
this is excellent. I actually think week 4 was the big step for me. This one kinda just built off of that one more. I don't understand everything in this one for sure, but I can definitely follow it enough to know what the sections of code are doing. It took me like 10 mins to add my old SFX into this engine and place the updated engine into my game.
Oh good! I was a little worried that this week might be too much all at once, but I cut it down as far as it would go. Glad to see that it turned out ok!
I was was thinking about blank notes or breaks and wondering if the easiest way to do it would to be just add a word byte to the note_table.i call BK and have it with a value of $0000. then when writing a song with a rest or something would be like
.byte D3,D4,BK,D4,D3,$FF
That should work for the Squares. I don't remember the exact number off the top of my head, but the Square channels output silence for any period below $006 or $007. I think I read somewhere that the Triangle channel will still produce audible output even at a period of $000, so you will have to do something else in the Triangle case. I'll see if I can fit Rests into the next lesson. I think the next lesson is a little easy, so adding an extra topic shouldn't hurt.
I'm learning a lot about pointers, lookup tables, and include from you which is just as useful as the music engine itself. I've always been a bit better at hacking existing code, then designing. I much prefer seeing code in use, then trying to figure out how to design it. After seeing this, it will probably help me a ton on my next game with all kinds of loads. Right now I have seperate subroutines to load my backgrounds and each one of them has the same sets of loops to load the background tiles and attribute table. Now that I kinda see how these lookup tables work, I could cut a ton of code out and use a single loading loop and use a table to decide which backgound and attributes to load.
Great! Aside from the sound-specific stuff, one of my goals with these tutorials is to introduce some advanced techniques that can be applied to other components of a game. Pretty much everything carries over. If you were going to write an RPG textbox engine for example, you'd have the same thing. You'd have a pointer table to all the text strings in the game. Talking to a villager would "load" one of the strings from the pointer table. Your text data would be divided into ranges: one range for characters, one for opcodes (next line, clear the box, play a sound, etc). Your data reading routine would test the ranges and branch to different sections of code. The method is the same, just the details are different.
Good call on the background loading routines. That's exactly how I'd do it. Setup a pointer table and have a single set of routines that will load and draw your background based on an index (background number).
I tend to be able to always make things work, but I design poorly. These sound tutorials will make me much more efficient in the future.
We all start out that way. You should see the code of my first project. It wasn't very pretty . But you learn a lot just trying stuff and tinkering with things. Once you get that experience under your belt, you can start looking at other peoples' code and figuring out better ways to do things. I learned most of these techniques from peeking at commercial games in FCEUX's debugger. But I wouldn't have been able to do that if I hadn't gotten my hands dirty with my own project first. I'm glad you are finding these tutorials helpful!
-------------------------
MetalSlime runs away
My nesdev blog: http://tummaigames.com/blog...
Edited: 09/17/2009 at 10:37 PM by MetalSlime
Sep 29, 2009 at 2:22:23 PM
udisi (88)
< King Solomon >
Posts: 3261 - Joined: 11/15/2006
United States
Profile
As I was saying I was looking at how you did the text strings, but I have a few questions about them to see if I can use the idea in my next game. Below is a screen shot from Wizardry and it uses the type of display I want to use.
I want to split the screen with the red circles and be able to update the red lines within those circles independently.
The top left would be used for some picture graphics, top right would be some test like the example, and the bottom would be some status and inventory info.
1) Now your text loading doesn't load a bunch of tiles. it pretty much just loads "playing", "not", "disabled", etc...so like maybe 10 tiles. I'm wondering is there a limit to the amount of tiles I can load using your method, or will I run out of vblank time. Kinda like how you can only load 64 sprites per frame?
2) You don't load any other background besides the text in your tutorial, so the background just fills the other space with $00. I want to put a border in like in the example with the white lines. Can I load the border once, and then just update the lines within the border? I know I can make strings that start and terminate at the spots I want, but I don't know if I can have a static border or not?
Pretty much that's all I want to do. Have that static boarder, and then be able to use text/graphic strings to fill in the areas of the boarders independently. You're code seems like it can be modified to do such, but I don't know if there's any hardware limitations to it.
Oct 1, 2009 at 3:53:32 AM
MetalSlime (0)
(Thomas Hjelm) < Crack Trooper >
Posts: 140 - Joined: 08/14/2008
Japan
Profile
Originally posted by: udisi
As I was saying I was looking at how you did the text strings, but I have a few questions about them to see if I can use the idea in my next game. Below is a screen shot from Wizardry and it uses the type of display I want to use.
I want to split the screen with the red circles and be able to update the red lines within those circles independently.
The top left would be used for some picture graphics, top right would be some test like the example, and the bottom would be some status and inventory info.
1) Now your text loading doesn't load a bunch of tiles. it pretty much just loads "playing", "not", "disabled", etc...so like maybe 10 tiles. I'm wondering is there a limit to the amount of tiles I can load using your method, or will I run out of vblank time. Kinda like how you can only load 64 sprites per frame?
2) You don't load any other background besides the text in your tutorial, so the background just fills the other space with $00. I want to put a border in like in the example with the white lines. Can I load the border once, and then just update the lines within the border? I know I can make strings that start and terminate at the spots I want, but I don't know if I can have a static border or not?
Pretty much that's all I want to do. Have that static boarder, and then be able to use text/graphic strings to fill in the areas of the boarders independently. You're code seems like it can be modified to do such, but I don't know if there's any hardware limitations to it.
To answer question 1, yes you can run out of vblank time if you fill your buffer with too many tile writes. You will probably have to stagger all of your text writing across several frames. Write some tiles, wait for the next frames, write some more, wait til the next frame, write some more, etc until you've written all the data you need to write. Use trial and error to figure out how many tiles you can safely write before you spill out of vblank.
Alternatively, if you don't mind the screen flashing black for a second, you can turn the PPU off, write everything at once and then turn it back on. If the PPU is off you don't have to pay attention to vblank at all (except for palette updates I think) so you can write as much to the screen in one go as you like. This would be an ok solution if the text inside the boxes wouldn't change after displaying it, like pressing the select button to load up a static stats screen. It would be a bad solution for something like a character selection screen, where pressing the d-pad left and right would select the next character and update the text with their individual stats. You'd get an annoying black flash in between every character.
For question 2, yes you should be able to draw the border once and then just update the lines within the borders. As long as you don't draw something in the same space as the border, or turn off the PPU or mess with the scroll, it should stay put.
-------------------------
MetalSlime runs away
My nesdev blog: http://tummaigames.com/blog...
Oct 3, 2009 at 1:43:33 PM
udisi (88)
< King Solomon >
Posts: 3261 - Joined: 11/15/2006
United States
Profile
Ok, yeah. I was wanting to try and load the backgound circles once, and since the areas are smaller inside them I was thinking I may be able to do all the updates in them without turning off the ppu. That link you posted about in the sound tutorials about NMI, said something about using the buffer and never turning off NMI.
I would think I could save a lot of PRG space if I only have to .db a few lines instead of a whole screen full of tiles. You know, like loading say 50 tiles instead of 256.
I know all about the flash from the disable/enable of NMI. It happens in my BattleBall game cause that's the way the Neardy Nights showed you how to do it. Also, I'm loading a full 256 tiles for every screen change, so I pretty much had no choice.
I'm just trying to plan out some more efficient ideas for my next game. If this works it'll save space and not have the screen flashes.
Oct 3, 2009 at 11:15:37 PM
MetalSlime (0)
(Thomas Hjelm) < Crack Trooper >
Posts: 140 - Joined: 08/14/2008
Japan
Profile
I think you're confusing "turning off the NMI" and "turning of the PPU" a little bit.
Turning off the NMI is done by clearing bit 7 of $2000, and will stop the NMI from interrupting the game at the beginning of each vblank.
Turning off the PPU is done by clearing bits 3 and 4 of $2001 (background and sprite rendering) and it will stop the PPU from rendering background tiles and sprites. In other words, turning the PPU off will mean that nothing gets drawn to the screen at all (black screen).
You can turn the PPU off and still have the NMI running. Any graphics updates you do while the PPU is off won't show until you turn it on again, but if you have any non-graphics code in the NMI (sound engine being a common example), it will still get run when it needs to be run.
Yeah, if you are loading a full 256 tiles all at once, turning off the PPU is the only way to do it. You can make it look smoother by fading your palettes to black before you turn the PPU off.
Anyway, to address your original problem, it is very possible to load the background circles once, and then be able to update the smaller insides without turning off the PPU. It just might take more than one frame depending on how many tile updates you have. You might have to do something like this:
update box 1
wait for next frame
update box 2
wait for next frame
update box 3
Each frame is only a fraction of a second, so the player won't be able to tell.
-------------------------
MetalSlime runs away
My nesdev blog: http://tummaigames.com/blog...
May 8, 2010 at 2:28:27 AM
rizz (9)
(Frank Westphal) < Eggplant Wizard >
Posts: 311 - Joined: 06/29/2007
Wisconsin
Profile
Hi MetalSlime/Thomas,
First & foremost, your sound lessons are fantastic. I've been liking the extra tips that you throw in as well, and your simplicity makes it easy to envision other areas of code that can benefit from your examples.
I'm writing to ask about this part of your code, which manually deals with 8-bit addition:
clc
adc stream_ptr_LO, x ;add Y to the LO pointer
sta stream_ptr_LO, x
bcc .end
inc stream_ptr_HI, x ;if there was a carry, add 1 to the HI pointer.
As far as I know, this could be done instead:
sta stream_ptr_LO, x
LDA stream_ptr_HI, x
ADC #$00
STA stream_ptr_HI, x
Was there any specific reason why you chose to check a branch condition?
-------------------------

My Development Blog

May 8, 2010 at 9:35:15 AM
MetalSlime (0)
(Thomas Hjelm) < Crack Trooper >
Posts: 140 - Joined: 08/14/2008
Japan
Profile
Originally posted by: rizz
Hi MetalSlime/Thomas,
First & foremost, your sound lessons are fantastic. I've been liking the extra tips that you throw in as well, and your simplicity makes it easy to envision other areas of code that can benefit from your examples.
I'm writing to ask about this part of your code, which manually deals with 8-bit addition:
clc
adc stream_ptr_LO, x ;add Y to the LO pointer
sta stream_ptr_LO, x
bcc .end
inc stream_ptr_HI, x ;if there was a carry, add 1 to the HI pointer.
As far as I know, this could be done instead:
sta stream_ptr_LO, x
LDA stream_ptr_HI, x
ADC #$00
STA stream_ptr_HI, x
Was there any specific reason why you chose to check a branch condition?
Thanks. Glad you find the tutorials helpful. As far as branching vs. adding 0, I didn't really have a specific reason to use one over the other. I'm just more accustomed to doing it the bcc way.
But now that I think about it, branching is probably slightly better performance-wise. A "bcc" and an "inc abs,x" take up 5 bytes of ROM space. "lda abs,x", "adc", "sta abs,x" takes up 8 bytes of ROM space. Most of the time the carry will be clear, so skipping instructions in the most common case saves some cpu cycles too. Even when the carry is set, the INC takes fewer cycles than the LDA/ADC/STA combo.
By the way, I had to look that up just now. I'm not a cycle counter so I don't know any of this stuff off the top of my head . I never considered any of this when I wrote the code. The savings are negligible, so it seems like a pick 'em to me.
-------------------------
MetalSlime runs away
My nesdev blog: http://tummaigames.com/blog...
Nov 20, 2016 at 3:21:27 AM
ubuntuyou (0)
(Joe ) < Cherub >
Posts: 9 - Joined: 10/24/2016
Nebraska
Profile
Is there any way I can still get all the attachments for these tutorials? I got the first one a few weeks ago but they don't seem to be available anymore. I tried following along with this one but I only get one repeating note that changes if I disable different channels and I'd like to compare. Thanks in advance.

Login
User Name:
Password
Stay logged in
Forget your login? Join NintendoAge!